#pragma once

#include <windows.h>
#include <string>
#include "sqlite3.h"
#include "Query.h"

using namespace Platform;

namespace mbase {

	delegate void FetchedObjectCallback(MetroClass^ obj);
	delegate Query^ CreateQueryBlock(String^ uid);
	delegate MetroClass^ CreateObjectBlock(String^ uid);

	static IMap<String^,CreateQueryBlock^>^ CreateQueryBlocks = ref new Map<String^,CreateQueryBlock^>;
	static IMap<String^,CreateObjectBlock^>^ CreateObjectBlocks = ref new Map<String^,CreateObjectBlock^>;

ref class Database sealed {
public:
	static void SetCreateQueryBlock(String^ className, CreateQueryBlock^ block) {
		CreateQueryBlocks->Insert(className, block);
	}
	static void SetCreateObjectBlock(String^ className, CreateObjectBlock^ block) {
		CreateObjectBlocks->Insert(className, block);
	}
private:
	sqlite3* db;
	/*static std::string ws2s(const std::wstring& ws) {
		std::string s(ws.begin(), ws.end());
		s.assign(ws.begin(), ws.end());
		return s;
	}
	static std::wstring s2ws(const std::string& s) {
		std::wstring ws(s.begin(), s.end());
		ws.assign(s.begin(), s.end());
		return ws;
	}*/
	bool executeSQL(String^ sql) {
		//char* errmsg = nullptr;
		//int rc = sqlite3_exec16(db, sql->Data(), nullptr, nullptr, &errmsg);
		sqlite3_stmt* stmt;
		int rc = sqlite3_prepare16(db, sql->Data(), -1, &stmt, 0);
		if (rc != SQLITE_OK) return false;
		if (sqlite3_step(stmt) == SQLITE_DONE) {
			sqlite3_finalize(stmt);
			return true;
		} else {
			sqlite3_finalize(stmt);
			OutputDebugStringW(L"Cannot execute statement without parameters\n");
			return false;
		}
		/*if (rc != SQLITE_OK) {
			OutputDebugStringW(s2ws(errmsg).c_str());
			sqlite3_free(errmsg);
			return false;
		} else {
			//OutputDebugStringW(L"SQL executed\n");
			return true;
		}*/
	}
	bool executeSQL(String^ sql, String^ param1) {
		sqlite3_stmt* stmt;
		int rc = sqlite3_prepare16(db, sql->Data(), -1, &stmt, 0);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, param1->Data(), -1, SQLITE_STATIC);
		if (rc != SQLITE_OK) return false;
		if (sqlite3_step(stmt) == SQLITE_DONE) {
			sqlite3_finalize(stmt);
			return true;
		} else {
			sqlite3_finalize(stmt);
			OutputDebugStringW(L"Cannot execute statement with parameter\n");
			return false;
		}
	}
	bool executeSQL(String^ sql, String^ param1, String^ param2) {
		sqlite3_stmt* stmt;
		int rc = sqlite3_prepare16(db, sql->Data(), -1, &stmt, 0);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, param1->Data(), -1, SQLITE_STATIC);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 2, param2->Data(), -1, SQLITE_STATIC);
		if (rc != SQLITE_OK) return false;
		if (sqlite3_step(stmt) == SQLITE_DONE) {
			sqlite3_finalize(stmt);
			return true;
		} else {
			sqlite3_finalize(stmt);
			OutputDebugStringW(L"Cannot execute statement with parameter\n");
			return false;
		}
	}
	bool executeSQL(String^ sql, String^ param1, String^ param2, String^ param3, String^ param4) {
		sqlite3_stmt* stmt;
		int rc = sqlite3_prepare16(db, sql->Data(), -1, &stmt, 0);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, param1->Data(), -1, SQLITE_STATIC);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 2, param2->Data(), -1, SQLITE_STATIC);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 3, param3->Data(), -1, SQLITE_STATIC);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 4, param4->Data(), -1, SQLITE_STATIC);
		if (rc != SQLITE_OK) return false;
		if (sqlite3_step(stmt) == SQLITE_DONE) {
			sqlite3_finalize(stmt);
			return true;
		} else {
			sqlite3_finalize(stmt);
			OutputDebugStringW(L"Cannot execute statement with parameter\n");
			return false;
		}
	}
	bool executeSQL(String^ sql, String^ param1, String^ param2, String^ param3, int param4) {
		sqlite3_stmt* stmt;
		int rc = sqlite3_prepare16(db, sql->Data(), -1, &stmt, 0);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, param1->Data(), -1, SQLITE_STATIC);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 2, param2->Data(), -1, SQLITE_STATIC);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 3, param3->Data(), -1, SQLITE_STATIC);
		if (rc == SQLITE_OK) rc = sqlite3_bind_int(stmt, 4, param4);
		if (rc != SQLITE_OK) return false;
		if (sqlite3_step(stmt) == SQLITE_DONE) {
			sqlite3_finalize(stmt);
			return true;
		} else {
			sqlite3_finalize(stmt);
			OutputDebugStringW(L"Cannot execute statement with parameter\n");
			return false;
		}
	}
	bool executeSQL(String^ sql, String^ param1, String^ param2, String^ param3, float param4) {
		sqlite3_stmt* stmt;
		int rc = sqlite3_prepare16(db, sql->Data(), -1, &stmt, 0);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, param1->Data(), -1, SQLITE_STATIC);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 2, param2->Data(), -1, SQLITE_STATIC);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 3, param3->Data(), -1, SQLITE_STATIC);
		if (rc == SQLITE_OK) rc = sqlite3_bind_double(stmt, 4, param4);
		if (rc != SQLITE_OK) return false;
		if (sqlite3_step(stmt) == SQLITE_DONE) {
			sqlite3_finalize(stmt);
			return true;
		} else {
			sqlite3_finalize(stmt);
			OutputDebugStringW(L"Cannot execute statement with parameter\n");
			return false;
		}
	}
public:
	static MetroClass^ CreateObject(String^ className, String^ uid) {
		auto block = CreateObjectBlocks->Lookup(className);
		return block(uid);
	}
	Database(String^ name) {
		auto path = Windows::Storage::ApplicationData::Current->LocalFolder->Path + "\\" + name;
		OutputDebugStringW((path + "\n")->Data());
		int rc = sqlite3_open16(path->Data(), &db);
		if (rc != SQLITE_OK) {
			OutputDebugStringW(L"Cannot open database\n");
			db = nullptr;
		} else {
			OutputDebugStringW(L"Database opened\n");
			executeSQL("CREATE TABLE IF NOT EXISTS objects (oid TEXT, classname TEXT)");
			executeSQL("CREATE TABLE IF NOT EXISTS strprops (oid TEXT, classname TEXT, propname TEXT, propvalue TEXT)");
			executeSQL("CREATE TABLE IF NOT EXISTS numprops (oid TEXT, classname TEXT, propname TEXT, propvalue INTEGER)");
			executeSQL("CREATE TABLE IF NOT EXISTS fltprops (oid TEXT, classname TEXT, propname TEXT, propvalue REAL)");
			executeSQL("CREATE TABLE IF NOT EXISTS objprops (oid TEXT, classname TEXT, propname TEXT, propvalue TEXT)");
			executeSQL("CREATE INDEX IF NOT EXISTS idx_objects ON objects (oid, classname)");
			executeSQL("CREATE INDEX IF NOT EXISTS idx_strprops ON strprops (oid, classname)");
			executeSQL("CREATE INDEX IF NOT EXISTS idx_numprops ON numprops (oid, classname)");
			executeSQL("CREATE INDEX IF NOT EXISTS idx_fltprops ON fltprops (oid, classname)");
			executeSQL("CREATE INDEX IF NOT EXISTS idx_objprops ON objprops (oid, classname)");
		}
	}
	~Database() {
		if (db != nullptr) {
			sqlite3_close(db);
			OutputDebugStringW(L"Database closed\n");
		} else OutputDebugStringW(L"Database is null - not closing\n");
	}
	bool BeginTransaction() {
		return executeSQL("BEGIN EXCLUSIVE TRANSACTION");
	}
	bool Commit() {
		return executeSQL("COMMIT");
	}
	bool Rollback() {
		return executeSQL("ROLLBACK");
	}
	IVector<MetroClass^>^ Fetch(Query^ query) {
		return Fetch(query, ref new Map<String^,MetroClass^>);
	}
	IVector<MetroClass^>^ Fetch(Query^ query, IMap<String^,MetroClass^>^ fetchedObjects) {
		auto result = ref new Vector<MetroClass^>;
		sqlite3_stmt* stmt;
		int rc;
		if (query->GetUID() != nullptr) {
			rc = sqlite3_prepare16(db, L"SELECT oid, classname FROM objects WHERE oid = ?", -1, &stmt, 0);
			if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, query->GetUID()->Data(), -1, SQLITE_STATIC);
		} else {
			rc = sqlite3_prepare16(db, L"SELECT oid, classname FROM objects WHERE classname = ?", -1, &stmt, 0);
			if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, query->GetClassName()->Data(), -1, SQLITE_STATIC);
		}
		if (rc != SQLITE_OK) return nullptr;
		while (sqlite3_step(stmt) == SQLITE_ROW) {
			const wchar_t* uid = (const wchar_t*) sqlite3_column_text16(stmt, 0);
			const wchar_t* clsName = (const wchar_t*) sqlite3_column_text16(stmt, 1);
			MetroClass^ obj = nullptr;
			if (query->GetClassName() == nullptr) {
				String^ className = ref new String(clsName);
				obj = CreateObject(className, ref new String(uid));
				if (nullptr == obj) {
					OutputDebugString((L"Don't know how to create object: " + className + "\n")->Data());
					return result;
				} //else OutputDebugStringW((L"Subobject created: " + className + " " + obj->ToString() + "\n")->Data());
			} else {
				obj = query->createObject(ref new String(uid));
			}
			result->Append(obj);
			fetchedObjects->Insert(obj->GetUID(), obj);
		}
		sqlite3_finalize(stmt);
		auto retVal = ref new Vector<MetroClass^>;
		if (result->Size > 0) {
			auto iter = result->First();
			do {
				auto obj = iter->Current;
				rc = sqlite3_prepare16(db, L"SELECT propname, propvalue FROM strprops WHERE oid = ?", -1, &stmt, 0);
				if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, obj->GetUID()->Data(), -1, SQLITE_STATIC);
				if (rc != SQLITE_OK) return nullptr;
				while (sqlite3_step(stmt) == SQLITE_ROW) {
					const wchar_t* key = (const wchar_t*) sqlite3_column_text16(stmt, 0);
					const wchar_t* value = (const wchar_t*) sqlite3_column_text16(stmt, 1);
					obj->GetStringProperties()->Insert(ref new String(key), ref new String(value));
				}
				sqlite3_finalize(stmt);
				rc = sqlite3_prepare16(db, L"SELECT propname, propvalue FROM numprops WHERE oid = ?", -1, &stmt, 0);
				if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, obj->GetUID()->Data(), -1, SQLITE_STATIC);
				if (rc != SQLITE_OK) return nullptr;
				while (sqlite3_step(stmt) == SQLITE_ROW) {
					const wchar_t* key = (const wchar_t*) sqlite3_column_text16(stmt, 0);
					int value = sqlite3_column_int(stmt, 1);
					obj->GetNumberProperties()->Insert(ref new String(key), ref new Number(value));
				}
				sqlite3_finalize(stmt);
				rc = sqlite3_prepare16(db, L"SELECT propname, propvalue FROM fltprops WHERE oid = ?", -1, &stmt, 0);
				if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, obj->GetUID()->Data(), -1, SQLITE_STATIC);
				if (rc != SQLITE_OK) return nullptr;
				while (sqlite3_step(stmt) == SQLITE_ROW) {
					const wchar_t* key = (const wchar_t*) sqlite3_column_text16(stmt, 0);
					float value = sqlite3_column_double(stmt, 1);
					obj->GetFloatProperties()->Insert(ref new String(key), ref new RealNumber(value));
				}
				sqlite3_finalize(stmt);
				rc = sqlite3_prepare16(db, L"SELECT propname, propvalue FROM objprops WHERE oid = ?", -1, &stmt, 0);
				if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, obj->GetUID()->Data(), -1, SQLITE_STATIC);
				if (rc != SQLITE_OK) return nullptr;
				while (sqlite3_step(stmt) == SQLITE_ROW) {
					const wchar_t* key = (const wchar_t*) sqlite3_column_text16(stmt, 0);
					const wchar_t* key2 = (const wchar_t*) sqlite3_column_text16(stmt, 1);
					//const wchar_t* clsName = (const wchar_t*) sqlite3_column_text16(stmt, 2);
					String^ uid2 = ref new String(key2);
					MetroClass^ obj2 = nullptr;
					if (!fetchedObjects->HasKey(uid2)) {
						Query^ query2 = ref new Query(nullptr, uid2);
						auto coll = Fetch(query2, fetchedObjects);
						if (coll->Size == 0) OutputDebugStringW(L"Oops-no object found\n");
						else {
							obj2 = coll->GetAt(0);
							//OutputDebugStringW((L"null-" + uid2 + " " + obj2->ToString() + "\n")->Data());
						}
					} else {
						obj2 = fetchedObjects->Lookup(uid2);
						//OutputDebugStringW((L"not null-" + obj->GetUID() + " " + uid2 + "\n")->Data());
					}
					if (obj2 != nullptr) obj->GetObjectProperties()->Insert(ref new String(key), obj2);
				}
				sqlite3_finalize(stmt);
				if (query->evaluateNatively(obj)) {
				//if (query->evaluate(obj)->asInt() != 0) {
					obj->DidInit();
					retVal->Append(obj);
				}
			} while (iter->MoveNext());
		}
		return retVal;
	}
	void Fetch(Query^ query, FetchedObjectCallback^ callback) {
		auto objects = Fetch(query);
		for (int i = 0; i < objects->Size; i++) {
			//auto obj = objects->GetAt(i);
			callback(objects->GetAt(i));
		}
		if (objects->Size > 0) {
			/*auto iter = objects->First();
			do {
				callback(iter->Current);
			} while (iter->MoveNext());*/
		}
	}
	bool Store(MetroClass^ obj) {
		return Store(obj, ref new Map<String^,bool>);
	}
	bool Store(MetroClass^ obj, IMap<String^,bool>^ storedObjects) {
		sqlite3_stmt* stmt;
		int rc = sqlite3_prepare16(db, L"SELECT * FROM objects WHERE oid = ?", -1, &stmt, 0);
		if (rc == SQLITE_OK) rc = sqlite3_bind_text16(stmt, 1, obj->GetUID()->Data(), -1, SQLITE_STATIC);
		if (rc != SQLITE_OK) return false;
		if (sqlite3_step(stmt) == SQLITE_ROW) {
			sqlite3_finalize(stmt);
			if (!executeSQL("DELETE FROM strprops WHERE oid = ?", obj->GetUID())) return false;
			if (!executeSQL("DELETE FROM numprops WHERE oid = ?", obj->GetUID())) return false;
			if (!executeSQL("DELETE FROM fltprops WHERE oid = ?", obj->GetUID())) return false;
			if (!executeSQL("DELETE FROM objprops WHERE oid = ?", obj->GetUID())) return false;
			OutputDebugStringW(L"Updating object\n");
		} else {
			sqlite3_finalize(stmt);
			if (!executeSQL("INSERT INTO objects (oid, classname) VALUES (?, ?)", obj->GetUID(), obj->GetClassName())) return false;
			OutputDebugStringW(L"Inserting object\n");
		}
		bool retVal = true;
		obj->WillStore();
		storedObjects->Insert(obj->GetUID(), true);
		auto me = this;
		obj->EnumerateStringProperties(ref new StringPropertyCallback([&](String^ key, String^ value) {
			//OutputDebugStringW((key + "=" + value + "\n")->Data());
			if (!me->executeSQL("INSERT INTO strprops (oid, classname, propname, propvalue) VALUES (?, ?, ?, ?)", obj->GetUID(), obj->GetClassName(), key, value)) retVal = false;
		}));
		obj->EnumerateNumberProperties(ref new NumberPropertyCallback([&](String^ key, int value) {
			//OutputDebugStringW((key + "=" + value + "\n")->Data());
			if (!me->executeSQL("INSERT INTO numprops (oid, classname, propname, propvalue) VALUES (?, ?, ?, ?)", obj->GetUID(), obj->GetClassName(), key, value)) retVal = false;
		}));
		obj->EnumerateFloatProperties(ref new FloatPropertyCallback([&](String^ key, float value) {
			//OutputDebugStringW((key + "=" + value + "\n")->Data());
			if (!me->executeSQL("INSERT INTO fltprops (oid, classname, propname, propvalue) VALUES (?, ?, ?, ?)", obj->GetUID(), obj->GetClassName(), key, value)) retVal = false;
		}));
		obj->EnumerateObjectProperties(ref new ObjectPropertyCallback([&](String^ key, Object^ value) {
			if (value != nullptr) {
			MetroClass^ obj2 = (MetroClass^) value;
			if (!storedObjects->HasKey(obj2->GetUID())) me->Store(obj2, storedObjects);
			else OutputDebugStringW(L"Won't store\n");
			if (!me->executeSQL("INSERT INTO objprops (oid, classname, propname, propvalue) VALUES (?, ?, ?, ?)", obj->GetUID(), obj->GetClassName(), key, obj2->GetUID())) retVal = false;
			OutputDebugStringW((L"Stored " + obj->GetUID() + " " + obj2->GetUID() + "\n")->Data());
			}
		}));
		return retVal;
	}
};

}
